{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "635d8ebb",
   "metadata": {},
   "source": [
    "# Agentic RAG\n",
    "\n",
    "- Author: [Heesun Moon](https://github.com/MoonHeesun)\n",
    "- Design: [LeeYuChul](https://github.com/LeeYuChul)\n",
    "- Peer Review:\n",
    "- Proofread : [Chaeyoon Kim](https://github.com/chaeyoonyunakim)\n",
    "- This is a part of [LangChain Open Tutorial](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/02-Structures/06-LangGraph-Agentic-RAG.ipynb)\n",
    "[![Open in GitHub](https://img.shields.io/badge/Open%20in%20GitHub-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/02-Structures/06-LangGraph-Agentic-RAG.ipynb)\n",
    "\n",
    "## Overview\n",
    "\n",
    "An **Agent** is useful when deciding whether to use a search tool. For more details about agents, refer to the [Agent](https://wikidocs.net/233782) page.\n",
    "\n",
    "To implement a search agent, simply grant the **LLM** access to the search tool.\n",
    "\n",
    "This can be integrated into [LangGraph](https://langchain-ai.github.io/langgraph/).\n",
    "\n",
    "![langgraph-agentic-rag](./assets/06-langgraph-agentic-rag.png)\n",
    "\n",
    "### Table of Contents\n",
    "\n",
    "- [Overview](#overview)\n",
    "- [Environment Setup](#environment-setup)\n",
    "- [Create a basic PDF-based Retrieval Chain](#create-a-basic-pdf-based-retrieval-chain)\n",
    "- [Defining AgentState](#defining-agentstate)\n",
    "- [Nodes and Edges](#nodes-and-edges)\n",
    "- [Graph](#graph)\n",
    "- [Execute the Graph](#execute-the-graph)\n",
    "\n",
    "### References\n",
    "\n",
    "- [LangGraph Tutorials](https://langchain-ai.github.io/langgraph/tutorials/)\n",
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6c7aba4",
   "metadata": {},
   "source": [
    "## Environment Setup\n",
    "\n",
    "Set up the environment. You may refer to [Environment Setup](https://wikidocs.net/257836) for more details.\n",
    "\n",
    "**[Note]**\n",
    "- ```langchain-opentutorial``` is a package that provides a set of easy-to-use environment setup, useful functions and utilities for tutorials. \n",
    "- You can checkout the [```langchain-opentutorial```](https://github.com/LangChain-OpenTutorial/langchain-opentutorial-pypi) for more details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "21943adb",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install langchain-opentutorial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f25ec196",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n",
      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.3.1\u001b[0m\n",
      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n"
     ]
    }
   ],
   "source": [
    "# Install required packages\n",
    "from langchain_opentutorial import package\n",
    "\n",
    "package.install(\n",
    "    [\n",
    "        \"langchain\",\n",
    "        \"langgraph\",\n",
    "        \"langchain_core\",\n",
    "        \"langchain_openai\",\n",
    "        \"pdfplumber\",\n",
    "        \"faiss-cpu\",\n",
    "    ],\n",
    "    verbose=False,\n",
    "    upgrade=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7f9065ea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Environment variables have been set successfully.\n"
     ]
    }
   ],
   "source": [
    "# Set environment variables\n",
    "from langchain_opentutorial import set_env\n",
    "\n",
    "set_env(\n",
    "    {\n",
    "        \"OPENAI_API_KEY\": \"\",\n",
    "        \"LANGCHAIN_API_KEY\": \"\",\n",
    "        \"LANGCHAIN_TRACING_V2\": \"true\",\n",
    "        \"LANGCHAIN_ENDPOINT\": \"https://api.smith.langchain.com\",\n",
    "        \"LANGCHAIN_PROJECT\": \"06-LangGraph-Agentic-RAG\",\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "690a9ae0",
   "metadata": {},
   "source": [
    "You can alternatively set API keys such as ```OPENAI_API_KEY``` in a ```.env``` file and load them.\n",
    "\n",
    "**[Note]** This is not necessary if you've already set the required API keys in previous steps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "4f99b5b6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Load API keys from .env file\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa00c3f4",
   "metadata": {},
   "source": [
    "## Create a basic PDF-based Retrieval Chain\n",
    "\n",
    "Here, we create a Retrieval Chain based on a PDF document. This is the Retrieval Chain with the simplest structure.\n",
    "\n",
    "However, in LangGraph, Retirever and Chain are created separately. Only then can detailed processing be performed for each node.\n",
    "\n",
    "**[Note]**\n",
    "- As this was covered in the previous tutorial, detailed explanation will be omitted."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "69cb77da",
   "metadata": {},
   "outputs": [],
   "source": [
    "from rag.pdf import PDFRetrievalChain\n",
    "\n",
    "# Load the PDF document\n",
    "pdf = PDFRetrievalChain(\n",
    "    [\"data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf\"]\n",
    ").create_chain()\n",
    "\n",
    "# Create retriever\n",
    "pdf_retriever = pdf.retriever\n",
    "\n",
    "# Create chain\n",
    "pdf_chain = pdf.chain"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03f629f2",
   "metadata": {},
   "source": [
    "Next, create the ```retriever_tool``` tool.\n",
    "\n",
    "**[Note]**\n",
    "\n",
    "The ```document_prompt``` is a prompt used to represent the retrieved document.\n",
    "\n",
    "**Available Keys**\n",
    "\n",
    "- ```page_content```\n",
    "- Keys in ```metadata```: (e.g.) ```source```, ```page```\n",
    "\n",
    "**Example Usage**\n",
    "\n",
    "```\"<document><context>{page_content}</context><metadata><source>{source}</source><page>{page}</page></metadata></document>\"```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "934332d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.tools.retriever import create_retriever_tool\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "\n",
    "# Create a retriever tool for querying the PDF document\n",
    "retriever_tool = create_retriever_tool(\n",
    "    pdf_retriever,\n",
    "    \"pdf_retriever\",\n",
    "    \"Analyze and provide insights from the PDF file titled *A European Approach to Artificial Intelligence - A Policy Perspective*. This document explores AI trends, challenges, and opportunities across various sectors, offering valuable policy recommendations for sustainable AI development in Europe.\",\n",
    "    document_prompt=PromptTemplate.from_template(\n",
    "        \"<document><context>{page_content}</context><metadata><source>{source}</source><page>{page}</page></metadata></document>\"\n",
    "    ),\n",
    ")\n",
    "\n",
    "# Add the retriever tool to the tools list for agent use\n",
    "tools = [retriever_tool]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14e47669",
   "metadata": {},
   "source": [
    "## Defining ```AgentState```\n",
    "\n",
    "We will define the ```AgentState``` .\n",
    "\n",
    "Each node is passed a ```state``` object. The ```state``` consists of a list of ```messages``` .\n",
    "\n",
    "Each node in the graph adds content to this list."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "0de4a970",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, Sequence, TypedDict\n",
    "from langchain_core.messages import BaseMessage\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "\n",
    "# Defines agent state and manages messages\n",
    "class AgentState(TypedDict):\n",
    "    # Manages the sequence of messages using the add_messages reducer function\n",
    "    messages: Annotated[Sequence[BaseMessage], add_messages]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b88d3e9f",
   "metadata": {},
   "source": [
    "## Nodes and Edges\n",
    "\n",
    "An agent-based RAG graph can be structured as follows:\n",
    "\n",
    "- ```state``` is a collection of messages.  \n",
    "- Each **node** updates (adds to) the ```state``` .  \n",
    "- **Conditional edges** determine the next node to visit.\n",
    "\n",
    "Now, let's create a simple **Grader**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "1298b865",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal\n",
    "from langchain import hub\n",
    "from langchain_core.messages import HumanMessage\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "from pydantic import BaseModel, Field\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "\n",
    "# Define the data model\n",
    "class grade(BaseModel):\n",
    "    \"\"\"A binary score for relevance checks\"\"\"\n",
    "\n",
    "    binary_score: str = Field(\n",
    "        description=\"Response 'yes' if the document is relevant to the question or 'no' if it is not.\"\n",
    "    )\n",
    "\n",
    "\n",
    "def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n",
    "    model = ChatOpenAI(temperature=0, model=\"gpt-4o-mini\", streaming=True)\n",
    "\n",
    "    # Set up LLM for structured output\n",
    "    llm_with_tool = model.with_structured_output(grade)\n",
    "\n",
    "    prompt = PromptTemplate(\n",
    "        template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
    "        Here is the retrieved document: \\n\\n {context} \\n\\n\n",
    "        Here is the user question: {question} \\n\n",
    "        If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
    "        Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n",
    "        input_variables=[\"context\", \"question\"],\n",
    "    )\n",
    "\n",
    "    chain = prompt | llm_with_tool\n",
    "\n",
    "    # Extract messages from the current state\n",
    "    messages = state[\"messages\"]\n",
    "\n",
    "    # Get the most recent message\n",
    "    last_message = messages[-1]\n",
    "\n",
    "    # Extract the original question\n",
    "    question = messages[0].content\n",
    "\n",
    "    retrieved_docs = last_message.content\n",
    "\n",
    "    # Perform relevance evaluation\n",
    "    scored_result = chain.invoke({\"question\": question, \"context\": retrieved_docs})\n",
    "\n",
    "    # Extract relevance status\n",
    "    score = scored_result.binary_score\n",
    "\n",
    "    if score == \"yes\":\n",
    "        print(\"==== [DECISION: DOCS RELEVANT] ====\")\n",
    "        return \"generate\"\n",
    "\n",
    "    else:\n",
    "        print(\"==== [DECISION: DOCS NOT RELEVANT] ====\")\n",
    "        print(score)\n",
    "        return \"rewrite\"\n",
    "\n",
    "\n",
    "def agent(state):\n",
    "    messages = state[\"messages\"]\n",
    "\n",
    "    model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o-mini\")\n",
    "\n",
    "    # Bind the retriever tool\n",
    "    model = model.bind_tools(tools)\n",
    "\n",
    "    # Generate agent response\n",
    "    response = model.invoke(messages)\n",
    "\n",
    "    # Returns as a list since it is appended to the existing list\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "def rewrite(state):\n",
    "    print(\"==== [QUERY REWRITE] ====\")\n",
    "    messages = state[\"messages\"]\n",
    "\n",
    "    question = messages[0].content\n",
    "\n",
    "    # Create a prompt for question refinement\n",
    "    msg = [\n",
    "        HumanMessage(\n",
    "            content=f\"\"\" \\n \n",
    "    Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n",
    "    Here is the initial question:\n",
    "    \\n ------- \\n\n",
    "    {question} \n",
    "    \\n ------- \\n\n",
    "    Formulate an improved question: \"\"\",\n",
    "        )\n",
    "    ]\n",
    "\n",
    "    # Refine the question using the LLM\n",
    "    model = ChatOpenAI(temperature=0, model=\"gpt-4o-mini\", streaming=True)\n",
    "    # Execute the Query-Transform chain\n",
    "    response = model.invoke(msg)\n",
    "\n",
    "    # Return the rewritten question\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "def generate(state):\n",
    "    messages = state[\"messages\"]\n",
    "\n",
    "    question = messages[0].content\n",
    "\n",
    "    docs = messages[-1].content\n",
    "\n",
    "    # Load the RAG prompt template\n",
    "    prompt = hub.pull(\"teddynote/rag-prompt\")\n",
    "\n",
    "    llm = ChatOpenAI(model_name=\"gpt-4o-mini\", temperature=0, streaming=True)\n",
    "\n",
    "    rag_chain = prompt | llm | StrOutputParser()\n",
    "\n",
    "    response = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
    "\n",
    "    return {\"messages\": [response]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7dc1b907",
   "metadata": {},
   "source": [
    "## Graph\n",
    "\n",
    "- Start with the ```call_model``` agent.  \n",
    "- The agent decides whether to call a function.  \n",
    "- If a function call is decided, an ```action``` is executed to invoke the tool (retriever).  \n",
    "- The tool's output is added to the messages ( ```state``` ), and the agent is called again.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "3a3bfb7c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END, StateGraph, START\n",
    "from langgraph.prebuilt import ToolNode, tools_condition\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "# Initialize the state graph workflow based on AgentState\n",
    "workflow = StateGraph(AgentState)\n",
    "\n",
    "# Define nodes\n",
    "workflow.add_node(\"agent\", agent)\n",
    "retrieve = ToolNode([retriever_tool])\n",
    "workflow.add_node(\"retrieve\", retrieve)\n",
    "workflow.add_node(\"rewrite\", rewrite)\n",
    "workflow.add_node(\n",
    "    # Response generation node after checking relevant documents\n",
    "    \"generate\",\n",
    "    generate,\n",
    ")\n",
    "\n",
    "# Connect edges\n",
    "workflow.add_edge(START, \"agent\")\n",
    "\n",
    "# Add conditional edges for determining whether to perform retrieval\n",
    "workflow.add_conditional_edges(\n",
    "    \"agent\",\n",
    "    # Evaluate agent decision\n",
    "    tools_condition,\n",
    "    {\n",
    "        # Map condition outputs to graph nodes\n",
    "        \"tools\": \"retrieve\",\n",
    "        END: END,\n",
    "    },\n",
    ")\n",
    "\n",
    "# Define edges for processing after action nodes are executed\n",
    "workflow.add_conditional_edges(\n",
    "    \"retrieve\",\n",
    "    # Evaluate document quality\n",
    "    grade_documents,\n",
    ")\n",
    "workflow.add_edge(\"generate\", END)\n",
    "workflow.add_edge(\"rewrite\", \"agent\")\n",
    "\n",
    "# Compile the graph\n",
    "graph = workflow.compile(checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aea3b646",
   "metadata": {},
   "source": [
    "Visualize the compiled graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "bac2d1d6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_opentutorial.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(graph)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9221791a",
   "metadata": {},
   "source": [
    "## Execute the Graph\n",
    "\n",
    "Now, let's run the graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3388e191",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  pdf_retriever (call_ntQvPrGfieUgf2wxlW6nwRUr)\n",
      " Call ID: call_ntQvPrGfieUgf2wxlW6nwRUr\n",
      "  Args:\n",
      "    query: application of AI in healthcare\n",
      "==================================================\n",
      "==== [DECISION: DOCS RELEVANT] ====\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mretrieve\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: pdf_retriever\n",
      "\n",
      "<document><context>activities. So far, however, AI applications in healthcare have been potential. Specific healthcare training should be provided to data\n",
      "confined to administrative tasks (i.e., Natural Language Processing scientists working in hospitals so that they can better understand</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>14</page></metadata></document>\n",
      "\n",
      "<document><context>are great, as more use of AI in research and development could\n",
      "Healthcare is arguably the sector where AI could make the lead to a more personalised healthcare based on patients’ data.</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>14</page></metadata></document>\n",
      "\n",
      "<document><context>intermediate / professional users (i.e., healthcare professionals). the safety of employees. The key application of AI is certainly in\n",
      "This is a matter of privacy and personal data protection, of building predictive maintenance. Yet, the more radical transformation of</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>10</page></metadata></document>\n",
      "\n",
      "<document><context>same. The Covid-19 crisis has shown how strained our National\n",
      "Healthcare Systems are, and AI solutions could help meet the cur- AI in the healthcare faces organisational and skill challenges. One</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>14</page></metadata></document>\n",
      "\n",
      "<document><context>very sensitive. An extensive use to feed AI tools can the use of patient’s data in the hospitals that deploy\n",
      "Health data raise many concerns. Data ownership is also an issue AI-powered applications. The patients should be aware</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></metadata></document>\n",
      "\n",
      "<document><context>Remote sible, as AI solutions can increasingly divert patients ning healthcare professionals, starting from the simple\n",
      "healthcare to appropriate solutions for their specific symptoms tasks and diagnostic appointments.\n",
      "and underlying conditions.\n",
      "16</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></metadata></document>\n",
      "\n",
      "<document><context>to extract information from clinical notes or predictive scheduling healthcare practitioners needs. In addition, at the regulatory le-\n",
      "of the visits) and diagnostic (machine and deep learning applied to vel it is important that new AI regulation is harmonised with other</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>14</page></metadata></document>\n",
      "\n",
      "<document><context>EIT Health and McKinsey & Company, (2020), Transforming healthcare with AI. Impact Scherer, M. (2016). Regulating Artificial Intelligence Systems: Risks, Challenges, Compe-</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>21</page></metadata></document>\n",
      "\n",
      "<document><context>advanced robots, autonomous cars, drones or Internet of Things place, a recent EIT Health Report envisages more in healthcare in\n",
      "applications)”. Broad AI definitions cover several technologies, in- the near future, such as remote monitoring, AI-powered alerting</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>3</page></metadata></document>\n",
      "\n",
      "<document><context>greatest impact in addressing societal challenges. Given rising de- A second challenge is that of finding a common language and un-\n",
      "mands and costs, AI could help doing more and better with the derstanding between data experts and healthcare professionals.</context><metadata><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>14</page></metadata></document>\n",
      "==================================================\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\Users\\user\\dev\\LangChain-OpenTutorial\\.venv\\Lib\\site-packages\\langsmith\\client.py:256: LangSmithMissingAPIKeyWarning: API key must be provided when using hosted LangSmith API\n",
      "  warnings.warn(\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mgenerate\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "The application of AI in healthcare has so far been confined primarily to administrative tasks, such as Natural Language Processing for extracting information from clinical notes and predictive scheduling. \n",
      "\n",
      "**Source**\n",
      "- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (page 14)\n",
      "==================================================\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.runnables import RunnableConfig\n",
    "from langchain_opentutorial.messages import stream_graph, invoke_graph, random_uuid\n",
    "\n",
    "# Configure settings (maximum recursion limit, thread_id)\n",
    "config = RunnableConfig(recursion_limit=10, configurable={\"thread_id\": random_uuid()})\n",
    "\n",
    "# Define the input data structure, including a user query about the type of agent memory\n",
    "inputs = {\n",
    "    \"messages\": [\n",
    "        (\n",
    "            \"user\",\n",
    "            \"Where has the application of AI in healthcare been confined to so far?\",\n",
    "        ),\n",
    "    ]\n",
    "}\n",
    "\n",
    "# Execute the graph\n",
    "invoke_graph(graph, inputs, config)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "e0549846",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "The application of AI in healthcare has so far been confined primarily to administrative tasks. This includes the use of Natural Language Processing (NLP) for extracting information from clinical notes and predictive scheduling for managing appointments and visits.\n",
      "\n",
      "**Source**\n",
      "- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (page 14)"
     ]
    }
   ],
   "source": [
    "# Graph Streaming Output\n",
    "stream_graph(graph, inputs, config, [\"agent\", \"rewrite\", \"generate\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed35081c",
   "metadata": {},
   "source": [
    "The following are examples of questions where document retrieval is **unnecessary**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "6d20ccf2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "The capital of South Korea is Seoul."
     ]
    }
   ],
   "source": [
    "# Examples of Questions Where Document Retrieval Is Unnecessary\n",
    "inputs = {\n",
    "    \"messages\": [\n",
    "        (\"user\", \"What is the capital of South Korea?\"),\n",
    "    ]\n",
    "}\n",
    "\n",
    "stream_graph(graph, inputs, config, [\"agent\", \"rewrite\", \"generate\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "78fb622a",
   "metadata": {},
   "source": [
    "Below are some examples of questions where document retrieval is not possible.\n",
    "\n",
    "As a result, a ```GraphRecursionError``` occurred during the continuous document retrieval process."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "95e25af9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "==== [DECISION: DOCS NOT RELEVANT] ====\n",
      "no\n",
      "==== [QUERY REWRITE] ====\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mrewrite\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "What are the key concepts and features covered in TeddyNote's LangChain tutorial, and how can they be applied in practical scenarios?\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "==== [DECISION: DOCS NOT RELEVANT] ====\n",
      "no\n",
      "==== [QUERY REWRITE] ====\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mrewrite\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "What are the key concepts and features covered in TeddyNote's LangChain tutorial, and how can they be applied in practical scenarios?\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "==== [DECISION: DOCS NOT RELEVANT] ====\n",
      "no\n",
      "==== [QUERY REWRITE] ====\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mrewrite\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "What are the key concepts and features covered in TeddyNote's LangChain tutorial, and how can they be applied in practical scenarios?\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "GraphRecursionError: Recursion limit of 10 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.\n",
      "For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT\n"
     ]
    }
   ],
   "source": [
    "from langgraph.errors import GraphRecursionError\n",
    "\n",
    "# Examples of Questions Where Document Retrieval Is Not Possible\n",
    "inputs = {\n",
    "    \"messages\": [\n",
    "        (\"user\", \"Tell me about TeddyNote's LangChain tutorial.\"),\n",
    "    ]\n",
    "}\n",
    "\n",
    "try:\n",
    "    stream_graph(graph, inputs, config, [\"agent\", \"rewrite\", \"generate\"])\n",
    "except GraphRecursionError as recursion_error:\n",
    "    print(f\"GraphRecursionError: {recursion_error}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0579be13",
   "metadata": {},
   "source": [
    "The next tutorial will cover how to resolve this issue."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
